<?php
/** Yz_SQL_Statement_Select
*
*/

class Yz_SQL_Statement_Select
    extends Yz_SQL_DMLStatement
{

    const CLAUSE_SELECT = 'SELECT';
    const CLAUSE_CALC_FOUND_ROWS = 'SQL_CALC_FOUND_ROWS';
    const CLAUSE_DISTINCT = 'DISTINCT';
    const CLAUSE_FIELDS = '_fields_';
    const CLAUSE_FROM = 'FROM';
    const CLAUSE_TABLES = '_tables_';
    const CLAUSE_WHERE = 'WHERE';
    const CLAUSE_GROUP_BY = 'GROUP BY';
    const CLAUSE_HAVING = 'HAVING';
    const CLAUSE_ORDER_BY = 'ORDER BY';
    const CLAUSE_LIMIT = 'LIMIT';
    const CLAUSE_FOR_UPDATE = 'FOR UPDATE';

    protected $clauses = array(
        self::CLAUSE_SELECT     => '',
        self::CLAUSE_DISTINCT   => '',
        self::CLAUSE_CALC_FOUND_ROWS => '',
        self::CLAUSE_FIELDS     => '',
        self::CLAUSE_FROM       => '',
        self::CLAUSE_TABLES     => '',
        self::CLAUSE_WHERE      => '',
        self::CLAUSE_GROUP_BY   => '',
        self::CLAUSE_HAVING     => '',
        self::CLAUSE_ORDER_BY   => '',
        self::CLAUSE_LIMIT      => '',
        self::CLAUSE_FOR_UPDATE => '',
    );


    /*****************************
     *      Public methods       *
     *****************************/

    /**
     *  @param varargs(Yz_SQL_Statement_Select)
     *  @return Yz_SQL_Statement_Select
     *  @todo such a setup that allows to apply order-by and limit to the UNION
     */
    public function union(Yz_SQL_Statement_Select $st)
    {
        $mysql = $this->getSQL();
        $unionsql = $st->getSQL();
        $select = new self($this->db);
        $select->sql = "($mysql)\nUNION\n($unionsql)";
        return $select;
    }

    /*****************************
     *         Fetching          *
     *****************************/


    /** fetch() : return one row
    *   @return <Yz_Hash>
    */
    public function fetch()
    {
        return $this->db->fetch();
    }

    /** fetchAll() : return all rows
    *   @return <Yz_Array>
    */
    public function fetchAll()
    {
        return $this->db->fetchAll();
    }

    /** fetchScalar() : return one row as scalar
    *   @return <scalar>
    */
    public function fetchScalar()
    {
        return $this->db->fetchScalar();
    }

    /** fetchIndexed() :
    *   @return Yz_Array_Interface
    */
    public function fetchIndexed()
    {
        throw NotImplementedError::_new_();
    }



    /***************************************
     ********** Constructor ****************/

    /**
     *  @param mixed $fields: either
     *      - a string, or
     *      - an array type, in which case the array's elements will be joined
     *  @return this
     *  @throw TypeError
     *  @throw ValueError
     */
    public function select( $fields )
    {
        if (is_string($fields)) {
            if ("" === $fields)
                throw ValueError::_new_();
        } else {
            $fields = new Yz_Hash($fields);
            if ($fields->isEmpty())
                throw ValueError::_new_();

            // NOTE can't use $this in closure
            $db = $this->db;
            $fields = $fields->apply(function($k, $v) use ($db) {
                return is_numeric($k) ? $v : $v . ' AS ' . $db->quoteIdentifier($k);
            })->join( ",\n  " );
        }
        $clause = "  " . $fields;
        $this->sql = null;
        $this->clauses[self::CLAUSE_SELECT] = self::CLAUSE_SELECT;
        $this->clauses[self::CLAUSE_FIELDS] = $clause;

        return $this;
    }

    public function calcFoundRows( $do=true )
    {
        $c = self::CLAUSE_CALC_FOUND_ROWS;
        $this->clauses[$c] = $do ? $c : "";
        return $this;
    }

    public function getFoundRows()
    {
        $db = $this->testQueryDone()->db;
        $c = self::CLAUSE_CALC_FOUND_ROWS;
        if ($this->clauses[$c] === "" ) {
            $c = $db->count();
        } else {
            $c = $db
                ->execute("SELECT FOUND_ROWS()")
                ->fetchScalar();
        }
        return $c;
    }

    /** build FROM clause, optionally with joined tables:
     *      accept array of tables, which will be processed by joinTables(),
     *      except the first entry whose key is the alias and value the table name
     *  @param string|array-type $table
     *  @param string|null $alias
     *  @return this
     */
    public function from( $table, $alias=null )
    {
        $tableList = Yz_Hash::valueOf($table);
        if ($tableList) {
            $table = current($tableList);
            $alias = key($tableList);
            unset($tableList[$alias]);
        }
        assert('is_string($table)');
        $this->_table(self::CLAUSE_FROM, $table, $alias);
        if ($tableList)
            foreach ($tableList as $alias => $table)
                if (is_string($table))
                    // use comma join, designated by NULL join type
                    $this->_joinTable(null, $table, $alias, null, null);
                else
                    // assume table to be Yz_SQL_Join instance
                    $this->joinTable( $table );
        return $this;
    }


    /**
     *  @param array-type[ (alias:) Yz_SQL_Join ] $tableList
     */
    public function joinTables( $tableList )
    {
        debug();
        return $this->_joinTables($tableList);
    }

    /**
     *  @param Yz_SQL_Join $args
     */
    public function joinTable( Yz_SQL_Join $table, $alias=null )
    {
        debug();
        return $this->_joinTable(
            $table->type,
            $table->table,
            $alias ?: $table->alias,
            $table->how,
            $table->conditions
        );
    }

    /** Add a table using LEFT JOIN
     *  @param string table
     *  @param string alias
     *  @param string how {ON|USING}
     *  @param string conds
     */
    public function leftJoin( $table, $alias, $how, $conds )
    {
        debug();
        return $this->_joinTable(
            self::LEFT_JOIN,
            $table,
            $alias,
            $how,
            $conds
        );
    }

    /** Add a table using INNER JOIN
     *  @param string table
     *  @param string alias
     *  @param string how {ON|USING}
     *  @param string conds
     */
    public function innerJoin( $table, $alias, $how, $conds )
    {
        debug();
        return $this->_joinTable(
            self::INNER_JOIN,
            $table,
            $alias,
            $how,
            $conds
        );
    }



    public function where( $what )
    {
        return $this->_where(self::CLAUSE_WHERE, $what, self::LOGICAL_AND);
    }

    public function orWhere( $what )
    {
        return $this->_where(self::CLAUSE_WHERE, $what, self::LOGICAL_OR);
    }

    public function groupBy( $what )
    {
        return $this->_groupBy(self::CLAUSE_GROUP_BY, $what);
    }

    public function having( $what )
    {
        return $this->_where(self::CLAUSE_HAVING, $what, self::LOGICAL_AND);
    }

    public function orderBy( $what )
    {
        return $this->_orderBy(self::CLAUSE_ORDER_BY, $what);
    }


    /**
     *  @param int offset: if param:limit is null, then param:offset is treated as limit, as in the SQL LIMIT clause
     *  @param int|null limit
     *
     */
    public function limit( $offset, $limit=null )
    {
        if (null === $offset)
            $offset = 0;
        else {
            $offset = Yz_Int::test($offset);
            if ($offset < 0)
                throw ValueError::_new_();
        }

        if (null === $limit) {
            $limit = $offset;
            $offset = 0;
        } else {
            $limit = Yz_Int::test($limit);
            if ($limit < 0)
                throw ValueError::_new_();
        }
        return $this->_limit($offset, $limit);
    }



    /**
     *  @return string SQL
     */
    protected function isComplete()
    {
        debug();

        $clauses = $this->clauses;

        return  "" !== $clauses[self::CLAUSE_SELECT]
            and "" !== $clauses[self::CLAUSE_FROM]
        ;
    } // testComplete


} // class

